/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.groups.filesystem;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.groups.EntityGroupImpl;
import org.apereo.portal.groups.EntityImpl;
import org.apereo.portal.groups.GroupServiceConfiguration;
import org.apereo.portal.groups.GroupsException;
import org.apereo.portal.groups.ICompositeGroupService;
import org.apereo.portal.groups.IEntity;
import org.apereo.portal.groups.IEntityGroup;
import org.apereo.portal.groups.IEntityGroupStore;
import org.apereo.portal.groups.IEntitySearcher;
import org.apereo.portal.groups.IEntityStore;
import org.apereo.portal.groups.IGroupMember;
import org.apereo.portal.groups.ILockableEntityGroup;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.services.GroupService;
import org.apereo.portal.spring.locator.EntityTypesLocator;

/**
 * This class is an <code>IEntityGroupStore</code> that uses the native file system for its back
 * end. It also implements <code>IEntityStore</code> and a no-op <code>IEntitySearcher</code>. You
 * can substitute a functional entity searcher by adding it to the group service element for this
 * component in the configuration document, <code>compositeGroupServices.xml</code>.
 *
 * <p>A groups file system looks like this:
 *
 * <p><code>
 * <hr width="100%">
 *  --&nbsp;groups root<br>
 * <blockquote>&nbsp;--&nbsp;org.apereo.portal.ChannelDefinition<br>
 *     <blockquote>&nbsp;--&nbsp;channel definition file<br>
 *                 &nbsp;--&nbsp;channel definition file<br>
 *        ...<br>
 *     </blockquote>
 * &nbsp;--&nbsp;org.apereo.portal.security.IPerson<br>
 *     <blockquote>&nbsp;--&nbsp;person directory<br>
 *         <blockquote>&nbsp;--&nbsp;person file <br>
 *                     &nbsp;--&nbsp;person file <br>
 *                     ...<br>
 *         </blockquote>
 *        &nbsp;--&nbsp;person directory <br>
 *     </blockquote>
 *      etc.<br>
 * </blockquote>
 * <hr width="100%">
 * </code>
 *
 * <p>The groups root is a file system directory declared in the group service configuration
 * document, where it is an attribute of the filesystem group service element. This directory has
 * sub-directories, each named for the underlying entity type that groups in that sub-directory
 * contain. If a service only contains groups of IPersons, the groups root would have 1
 * sub-directory named org.apereo.portal.security.IPerson.
 *
 * <p>A directory named for a type may contain both sub-directories and files. The sub-directories
 * represent groups that can contain other groups. The files represent groups that can contain
 * entity as well as group members. The files contain keys, one to a line, and look like this:
 *
 * <p><code>
 * <hr width="100%">
 * #&nbsp;this is a comment<br>
 * #&nbsp;another comment<br>
 * <br>
 * key1 Key One<br>
 * key2<br>
 * group:org$jasig$portal$security$IPerson/someDirectory/someFile<br>
 * key3<br>
 * &nbsp;# comment <br>
 * <hr width="100%">
 * </code>
 *
 * <p>Blank lines and lines that start with the <code>COMMENT</code> String (here <code>#</code>)
 * are ignored. The first token on a non-ignored line is assumed to be a group member key. If the
 * key starts with the <code>GROUP_PREFIX</code> (here <code>:group</code>), it is treated as a
 * local group key. Otherwise, it is assumed to be an entity key. The rest of the tokens on the line
 * are ignored.
 *
 * <p>The file above contains 3 entity keys, <code>key1</code>, <code>key2</code>, and <code>key3
 * </code>, and 1 group key, <code>org$jasig$portal$security$IPerson/someDirectory/someFile</code>.
 * It represents a group with 3 entity members and 1 group member. The local key of a group is its
 * file path starting at the type name, with the <code>FileSystemGroupStore.SUBSTITUTE_PERIOD</code>
 * character substituted for the real period character.
 *
 * <p>The store is not implemented as a singleton, so you can have multiple concurrent instances
 * pointing to different groups root directories.
 */
public class FileSystemGroupStore implements IEntityGroupStore, IEntityStore, IEntitySearcher {
    private static final Log log = LogFactory.getLog(FileSystemGroupStore.class);
    // File system constants for unix/windows compatibility:
    protected static char FORWARD_SLASH = '/';
    protected static char BACK_SLASH = '\\';

    // Group file constants:
    protected static String COMMENT = "#";
    protected static String GROUP_PREFIX = "group:";

    // The period is legal in filesystem names but could conflict with
    // the node separator in the group key.
    protected static char PERIOD = '.';
    protected static char SUBSTITUTE_PERIOD = '$';
    protected boolean useSubstitutePeriod = false;

    private static String DEBUG_CLASS_NAME = "FileSystemGroupStore";

    // Path to groups root directory.
    private String groupsRootPath;

    // Either back slash or forward slash.
    protected char goodSeparator;
    protected char badSeparator;

    // Cache of retrieved groups.
    private Map cache;

    private FilenameFilter fileFilter = new FileFilter();

    private Class defaultEntityType;

    // Value holder adds last modified timestamp.
    private class GroupHolder {
        private long lastModified = 0;
        private IEntityGroup group;

        protected GroupHolder(IEntityGroup g, long lm) {
            this.group = g;
            this.lastModified = lm;
        }

        protected IEntityGroup getGroup() {
            return group;
        }

        protected long getLastModified() {
            return lastModified;
        }
    }

    private class FileFilter implements FilenameFilter {
        /**
         * Tests if a specified file should be included in a file list.
         *
         * @param dir the directory in which the file was found.
         * @param name the name of the file.
         * @return <code>true</code> if and only if the name should be included in the file list;
         *     <code>false</code> otherwise.
         */
        @Override
        public boolean accept(File dir, String name) {
            return (!name.startsWith("#"))
                    && (!name.startsWith("%"))
                    && (!name.startsWith("."))
                    && (!name.endsWith("~"))
                    && (!name.endsWith(".tmp"))
                    && (!name.endsWith(".temp"))
                    && (!name.endsWith(".txt"));
        }
    }

    /** FileSystemGroupStore constructor. */
    public FileSystemGroupStore() {
        this(null);
    }
    /** FileSystemGroupStore constructor. */
    public FileSystemGroupStore(GroupServiceConfiguration cfg) {
        super();
        initialize(cfg);
    }
    /** @return GroupHolder */
    protected GroupHolder cacheGet(String key) {
        return (GroupHolder) getCache().get(key);
    }

    protected void cachePut(String key, Object val) {
        getCache().put(key, val);
    }

    protected String conformSeparatorChars(String s) {
        return s.replace(getBadSeparator(), getGoodSeparator());
    }
    /**
     * Delete this <code>IEntityGroup</code> from the data store. We assume that groups will be
     * deleted via the file system, not the group service.
     *
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public void delete(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException("FileSystemGroupStore.delete() not supported");
    }
    /**
     * Returns an instance of the <code>IEntityGroup</code> from the data store.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     * @param file java.io.File
     */
    private IEntityGroup find(File file) throws GroupsException {
        return find(getKeyFromFile(file));
    }
    /**
     * Returns an instance of the <code>IEntityGroup</code> from the data store.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     * @param key java.lang.String
     */
    @Override
    public IEntityGroup find(String key) throws GroupsException {
        if (log.isDebugEnabled()) {
            log.debug(DEBUG_CLASS_NAME + ".find(): group key: " + key);
        }

        String path = getFilePathFromKey(key);
        File f = new File(path);

        GroupHolder groupHolder = cacheGet(key);

        if (groupHolder == null || (groupHolder.getLastModified() != f.lastModified())) {
            if (log.isDebugEnabled()) {
                log.debug(
                        DEBUG_CLASS_NAME
                                + ".find(): retrieving group from file system for "
                                + path);
            }

            if (!f.exists()) {
                if (log.isDebugEnabled()) {
                    log.debug(DEBUG_CLASS_NAME + ".find(): file does not exist: " + path);
                }
                return null;
            }

            IEntityGroup group = newInstance(f);
            groupHolder = new GroupHolder(group, f.lastModified());
            cachePut(key, groupHolder);
        }
        return groupHolder.getGroup();
    }
    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of <code>IEntityGroups
     * </code> that the <code>IEntity</code> belongs to.
     *
     * @return java.util.Iterator
     * @param ent org.apereo.portal.groups.IEntityGroup
     */
    protected Iterator findParentGroups(IEntity ent) throws GroupsException {
        if (log.isDebugEnabled()) log.debug(DEBUG_CLASS_NAME + ".findParentGroups(): for " + ent);

        List groups = new ArrayList();
        File root = getFileRoot(ent.getType());
        if (root != null) {
            File[] files = getAllFilesBelow(root);

            try {
                for (int i = 0; i < files.length; i++) {
                    Collection ids = getEntityIdsFromFile(files[i]);
                    if (ids.contains(ent.getKey())) {
                        groups.add(find(files[i]));
                    }
                }
            } catch (IOException ex) {
                throw new GroupsException("Problem reading group files", ex);
            }
        }

        return groups.iterator();
    }
    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of <code>IEntityGroups
     * </code> that the <code>IGroupMember</code> belongs to.
     *
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    protected Iterator findParentGroups(IEntityGroup group) throws GroupsException {
        if (log.isDebugEnabled()) log.debug(DEBUG_CLASS_NAME + ".findParentGroups(): for " + group);

        List groups = new ArrayList();
        {
            String typeName = group.getLeafType().getName();
            File parent = getFile(group).getParentFile();
            if (!parent.getName().equals(typeName)) {
                groups.add(find(parent));
            }

            File root = getFileRoot(group.getLeafType());
            File[] files = getAllFilesBelow(root);
            try {
                for (int i = 0; i < files.length; i++) {
                    Collection ids = getGroupIdsFromFile(files[i]);
                    if (ids.contains(group.getLocalKey())) {
                        groups.add(find(files[i]));
                    }
                }
            } catch (IOException ex) {
                throw new GroupsException("Problem reading group files", ex);
            }
        }
        return groups.iterator();
    }
    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of <code>IEntityGroups
     * </code> that the <code>IGroupMember</code> belongs to.
     *
     * @return java.util.Iterator
     * @param gm org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public Iterator findParentGroups(IGroupMember gm) throws GroupsException {
        if (gm.isGroup()) {
            IEntityGroup group = (IEntityGroup) gm;
            return findParentGroups(group);
        } else {
            IEntity ent = (IEntity) gm;
            return findParentGroups(ent);
        }
    }
    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of <code>IEntities</code>
     * that are members of this <code>IEntityGroup</code>.
     *
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public java.util.Iterator findEntitiesForGroup(IEntityGroup group) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(
                    DEBUG_CLASS_NAME
                            + ".findEntitiesForGroup(): retrieving entities for group "
                            + group);

        Collection entities = null;
        File f = getFile(group);
        if (f.isDirectory()) {
            entities = Collections.EMPTY_LIST;
        } else {
            entities = getEntitiesFromFile(f);
        }

        return entities.iterator();
    }
    /**
     * Returns an instance of the <code>ILockableEntityGroup</code> from the data store.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     * @param key java.lang.String
     */
    @Override
    public ILockableEntityGroup findLockable(String key) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".findLockable() not supported");
    }
    /**
     * Returns a <code>String[]</code> containing the keys of <code>IEntityGroups</code> that are
     * members of this <code>IEntityGroup</code>. In a composite group system, a group may contain a
     * member group from a different service. This is called a foreign membership, and is only
     * possible in an internally-managed service. A group store in such a service can return the key
     * of a foreign member group, but not the group itself, which can only be returned by its local
     * store.
     *
     * @return String[]
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public java.lang.String[] findMemberGroupKeys(IEntityGroup group) throws GroupsException {
        String[] keys;
        File f = getFile(group);
        if (f.isDirectory()) {
            File[] files = f.listFiles();
            keys = new String[files.length];
            for (int i = 0; i < files.length; i++) {
                keys[i] = getKeyFromFile(files[i]);
            }
        } else {
            try {
                Collection groupKeys = getGroupIdsFromFile(f);
                keys = (String[]) groupKeys.toArray(new String[groupKeys.size()]);
            } catch (IOException ex) {
                throw new GroupsException(
                        DEBUG_CLASS_NAME
                                + ".findMemberGroupKeys(): "
                                + "problem finding group members",
                        ex);
            }
        }
        return keys;
    }
    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of <code>IEntityGroups
     * </code> that are members of this <code>IEntityGroup</code>.
     *
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public java.util.Iterator findMemberGroups(IEntityGroup group) throws GroupsException {
        String[] keys = findMemberGroupKeys(group); // No foreign groups here.
        List groups = new ArrayList(keys.length);
        for (int i = 0; i < keys.length; i++) {
            groups.add(find(keys[i]));
        }
        return groups.iterator();
    }
    /**
     * Recursive search of directories underneath dir for files that match filter.
     *
     * @return java.util.Set
     */
    public Set getAllDirectoriesBelow(File dir) {
        Set allDirectories = new HashSet();
        if (dir.isDirectory()) {
            primGetAllDirectoriesBelow(dir, allDirectories);
        }
        return allDirectories;
    }
    /** Recursive search of directories underneath dir for files that match filter. */
    public File[] getAllFilesBelow(File dir) {
        Set allFiles = new HashSet();
        if (dir.isDirectory()) {
            primGetAllFilesBelow(dir, allFiles);
        }
        return (File[]) allFiles.toArray(new File[allFiles.size()]);
    }
    /**
     * Returns the filesystem separator character NOT in use.
     *
     * @return char
     */
    protected char getBadSeparator() {
        return badSeparator;
    }
    /** @return java.util.Map */
    protected java.util.Map getCache() {
        return cache;
    }
    /**
     * Returns a Class representing the default entity type.
     *
     * @return Class
     */
    protected Class getDefaultEntityType() {
        return defaultEntityType;
    }
    /**
     * @param idFile java.io.File - a file of ids.
     * @return entities Collection.
     */
    protected Collection getEntitiesFromFile(File idFile) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntitiesFromFile(): for " + idFile.getPath());

        Collection ids = null;
        Class type = getEntityType(idFile);
        if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) {
            throw new GroupsException("Invalid entity type: " + type);
        }
        try {
            ids = getEntityIdsFromFile(idFile);
        } catch (Exception ex) {
            throw new GroupsException("Problem retrieving keys from file", ex);
        }

        Collection entities = new ArrayList(ids.size());

        for (Iterator itr = ids.iterator(); itr.hasNext(); ) {
            String key = (String) itr.next();
            entities.add(GroupService.getEntity(key, type));
        }

        if (log.isDebugEnabled())
            log.debug(
                    DEBUG_CLASS_NAME
                            + "getEntitiesFromFile(): Retrieved "
                            + entities.size()
                            + " entities");

        return entities;
    }
    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getEntityIdsFromFile(File idFile)
            throws IOException, FileNotFoundException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntityIdsFromFile(): Reading " + idFile.getPath());

        Collection ids = getIdsFromFile(idFile, false);

        if (log.isDebugEnabled())
            log.debug(
                    DEBUG_CLASS_NAME + "getEntityIdsFromFile(): Retrieved " + ids.size() + " IDs");

        return ids;
    }
    /**
     * @param f File
     * @return java.lang.Class The Class is the first node of the full path name.
     */
    protected Class getEntityType(File f) {
        String path = f.getPath();
        String afterRootPath = null;
        Class type = null;
        if (path.startsWith(getGroupsRootPath())) {
            afterRootPath = path.substring(getGroupsRootPath().length());
            int end = afterRootPath.indexOf(File.separatorChar);
            String typeName = afterRootPath.substring(0, end);

            try {
                type = Class.forName(typeName);
            } catch (ClassNotFoundException cnfe) {
            }
        }
        return type;
    }
    /**
     * @param group IEntityGroup.
     * @return File
     */
    protected File getFile(IEntityGroup group) {
        String key = getFilePathFromKey(group.getLocalKey());
        return new File(key);
    }

    protected String getFilePathFromKey(String key) {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".getFilePathFromKey(): for key: " + key);

        String groupKey = useSubstitutePeriod ? key.replace(SUBSTITUTE_PERIOD, PERIOD) : key;

        String fullKey = getGroupsRootPath() + groupKey;

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".getFilePathFromKey(): full key: " + fullKey);

        return conformSeparatorChars(fullKey);
    }
    /** Returns a File that is the root for groups of the given type. */
    protected File getFileRoot(Class type) {
        String path = getGroupsRootPath() + type.getName();
        File f = new File(path);
        return (f.exists()) ? f : null;
    }
    /**
     * Returns the filesystem separator character in use.
     *
     * @return char
     */
    protected char getGoodSeparator() {
        return goodSeparator;
    }
    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getGroupIdsFromFile(File idFile)
            throws IOException, FileNotFoundException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getGroupIdsFromFile(): Reading " + idFile.getPath());

        Collection ids = getIdsFromFile(idFile, true);

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getGroupIdsFromFile(): Retrieved " + ids.size() + " IDs");

        return ids;
    }
    /** @return java.lang.String */
    public java.lang.String getGroupsRootPath() {
        return groupsRootPath;
    }
    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getIdsFromFile(File idFile, boolean groupIds)
            throws IOException, FileNotFoundException {
        Collection ids = new HashSet();
        BufferedReader br = Files.newBufferedReader(idFile.toPath(), UTF_8);
        String line, tok;

        line = br.readLine();
        while (line != null) {
            line = line.trim();
            if (!line.startsWith(COMMENT) && (line.length() > 0)) {
                StringTokenizer st = new StringTokenizer(line);
                tok = st.nextToken();
                if (tok != null) {
                    if (tok.startsWith(GROUP_PREFIX)) {
                        if (groupIds) {
                            ids.add(tok.substring(GROUP_PREFIX.length()));
                        }
                    } else {
                        if (!groupIds) {
                            ids.add(tok);
                        }
                    }
                }
            }
            line = br.readLine();
        }
        br.close();

        return ids;
    }

    protected String getKeyFromFile(File f) {
        String key = null;
        if (f.getPath().startsWith(getGroupsRootPath())) {
            key = f.getPath().substring(getGroupsRootPath().length());

            if (useSubstitutePeriod) {
                key = key.replace(PERIOD, SUBSTITUTE_PERIOD);
            }
        }
        return key;
    }

    protected void initialize(GroupServiceConfiguration cfg) {
        cache = Collections.synchronizedMap(new HashMap());

        goodSeparator = File.separatorChar;
        badSeparator = (goodSeparator == FORWARD_SLASH) ? BACK_SLASH : FORWARD_SLASH;

        defaultEntityType = IPerson.class;
        GroupServiceConfiguration config = cfg;
        if (config == null) {
            try {
                config = GroupServiceConfiguration.getConfiguration();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }

        String sep = config.getNodeSeparator();
        if (sep != null) {
            String period = String.valueOf(PERIOD);
            useSubstitutePeriod = sep.equals(period);
        }
    }
    /** @return org.apereo.portal.groups.IEntityGroup */
    private IEntityGroup newInstance(File f) throws GroupsException {
        String key = getKeyFromFile(f);
        String name = f.getName();
        Class cl = getEntityType(f);
        return newInstance(key, cl, name);
    }
    /**
     * @return org.apereo.portal.groups.IEntityGroup We assume that new groups will be created
     *     updated via the file system, not the group service.
     */
    @Override
    public IEntityGroup newInstance(Class entityType) throws GroupsException {
        throw new UnsupportedOperationException(
                DEBUG_CLASS_NAME + ".newInstance(Class cl) not supported");
    }

    @Override
    public IEntity newInstance(String key, Class type) throws GroupsException {
        if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) {
            throw new GroupsException("Invalid group type: " + type);
        }
        return new EntityImpl(key, type);
    }
    /** @return org.apereo.portal.groups.IEntityGroup */
    private IEntityGroup newInstance(String newKey, Class newType, String newName)
            throws GroupsException {
        EntityGroupImpl egi = new EntityGroupImpl(newKey, newType);
        egi.primSetName(newName);
        return egi;
    }
    /** Returns all directories under dir. */
    private void primGetAllDirectoriesBelow(File dir, Set allDirectories) {
        File[] files = dir.listFiles(fileFilter);
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                primGetAllDirectoriesBelow(files[i], allDirectories);
                allDirectories.add(files[i]);
            }
        }
    }
    /** Returns all files (not directories) underneath dir. */
    private void primGetAllFilesBelow(File dir, Set allFiles) {
        File[] files = dir.listFiles(fileFilter);
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                primGetAllFilesBelow(files[i], allFiles);
            } else {
                allFiles.add(files[i]);
            }
        }
    }
    /**
     * Find EntityIdentifiers for entities whose name matches the query string according to the
     * specified method and is of the specified type
     */
    @Override
    public EntityIdentifier[] searchForEntities(String query, SearchMethod method, Class type)
            throws GroupsException {
        return new EntityIdentifier[0];
    }
    /**
     * Returns an EntityIdentifier[] of groups of the given leaf type whose names match the query
     * string according to the search method.
     *
     * <p>Treats case sensitive and case insensitive searches the same.
     *
     * @param query String the string used to match group names.
     * @param searchMethod see org.apereo.portal.groups.IGroupConstants.
     * @param leafType the leaf type of the groups we are searching for.
     * @return EntityIdentifier[]
     */
    @Override
    public EntityIdentifier[] searchForGroups(
            String query, SearchMethod searchMethod, Class leafType) throws GroupsException {
        List ids = new ArrayList();
        File baseDir = getFileRoot(leafType);

        if (log.isDebugEnabled())
            log.debug(
                    DEBUG_CLASS_NAME
                            + "searchForGroups(): "
                            + query
                            + " method: "
                            + searchMethod
                            + " type: "
                            + leafType);

        if (baseDir != null) {
            String nameFilter = null;

            switch (searchMethod) {
                case DISCRETE:
                case DISCRETE_CI:
                    nameFilter = query;
                    break;
                case STARTS_WITH:
                case STARTS_WITH_CI:
                    nameFilter = query + ".*";
                    break;
                case ENDS_WITH:
                case ENDS_WITH_CI:
                    nameFilter = ".*" + query;
                    break;
                case CONTAINS:
                case CONTAINS_CI:
                    nameFilter = ".*" + query + ".*";
                    break;
                default:
                    throw new GroupsException(
                            DEBUG_CLASS_NAME
                                    + ".searchForGroups(): Unknown search method: "
                                    + searchMethod);
            }

            final Pattern namePattern = Pattern.compile(nameFilter);
            final FilenameFilter filter =
                    new FilenameFilter() {
                        @Override
                        public boolean accept(File dir, String name) {
                            return namePattern.matcher(name).matches();
                        }
                    };

            Set allDirs = getAllDirectoriesBelow(baseDir);
            allDirs.add(baseDir);

            for (Iterator itr = allDirs.iterator(); itr.hasNext(); ) {
                File[] files = ((File) itr.next()).listFiles(filter);
                for (int filesIdx = 0; filesIdx < files.length; filesIdx++) {
                    String key = getKeyFromFile(files[filesIdx]);
                    EntityIdentifier ei =
                            new EntityIdentifier(key, ICompositeGroupService.GROUP_ENTITY_TYPE);
                    ids.add(ei);
                }
            }
        }

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".searchForGroups(): found " + ids.size() + " files.");

        return (EntityIdentifier[]) ids.toArray(new EntityIdentifier[ids.size()]);
    }
    /** @param newCache java.util.Map */
    protected void setCache(java.util.Map newCache) {
        cache = newCache;
    }
    /** @param newGroupsRootPath java.lang.String */
    protected void setGroupsRootPath(java.lang.String newGroupsRootPath) {
        groupsRootPath = conformSeparatorChars(newGroupsRootPath) + getGoodSeparator();
    }
    /**
     * Adds or updates the <code>IEntityGroup</code> AND ITS MEMBERSHIPS to the data store, as
     * appropriate. We assume that groups will be updated via the file system, not the group
     * service.
     *
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public void update(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".update() not supported");
    }
    /**
     * Commits the group memberships of the <code>IEntityGroup</code> to the data store. We assume
     * that groups will be updated via the file system, not the group service.
     *
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    @Override
    public void updateMembers(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException(
                DEBUG_CLASS_NAME + ".updateMembers() not supported");
    }
    /**
     * Answers if <code>group</code> contains <code>member</code>.
     *
     * @return boolean
     * @param group org.apereo.portal.groups.IEntityGroup
     * @param member org.apereo.portal.groups.IGroupMember
     */
    @Override
    public boolean contains(IEntityGroup group, IGroupMember member) throws GroupsException {
        File f = getFile(group);
        return (f.isDirectory()) ? directoryContains(f, member) : fileContains(f, member);
    }
    /**
     * Answers if <code>file</code> contains <code>member</code>.
     *
     * @param file
     * @param member
     * @return boolean
     */
    private boolean fileContains(File file, IGroupMember member) throws GroupsException {
        Collection ids = null;
        try {
            ids = member.isGroup() ? getGroupIdsFromFile(file) : getEntityIdsFromFile(file);
        } catch (Exception ex) {
            throw new GroupsException("Error retrieving ids from file", ex);
        }
        return ids.contains(member.getKey());
    }

    /**
     * Answers if <code>directory</code> contains <code>member</code>. A directory can only contain
     * (other) groups.
     *
     * @param directory java.io.File
     * @param member
     * @return boolean
     */
    private boolean directoryContains(File directory, IGroupMember member) {
        boolean found = false;
        if (member.isGroup()) {
            File memberFile = getFile((IEntityGroup) member);
            File[] files = directory.listFiles();
            for (int i = 0; i < files.length & !found; i++) {
                found = files[i].equals(memberFile);
            }
        }
        return found;
    }
}
